<?php
/**
 * 微信基础接口
 */
namespace App\Ext\Wechat;

use Exception;

class Common
{
    /**
     * 获取 access token
     * @param $appID 第三方用户唯一凭证
     * @param $appSecret 第三方用户唯一凭证密钥 appsecret
     * @return mixed
     *
     * 正常情况下，微信会返回下述JSON数据包给公众号：
     *
     * {"access_token":"ACCESS_TOKEN","expires_in":7200}
     *
     * 错误时微信会返回错误码等信息，JSON数据包示例如下（该示例为AppID无效错误）:
     *
     * {"errcode":40013,"errmsg":"invalid appid"}
     *
     */
    public static function accessToken($appID, $appSecret)
    {
        if (empty($appID)) die('Wechat.Common.accessToken: $appID is not defined');
        if (empty($appSecret)) die('Wechat.Common.accessToken: $appSecret is not defined');
        $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . $appID . '&secret=' . $appSecret;
        $callback = self::get($url);
        $callback = json_decode($callback);
        if (empty($callback->errcode)) {
            return $callback;
        } else {
            die('Wechat.Common.accessToken: ' . $callback->errcode . ' - ' . $callback->errmsg);
        }
        return false;
    }

    /**
     * 获取微信服务器 IP 地址
     * 如果公众号基于安全等考虑，需要获知微信服务器的 IP 地址列表，以便进行相关限制，可以通过该接口获得微信服务器 IP 地址列表。
     * @param $accessToken
     * @return bool|string
     *
     * 正常情况下，微信会返回下述 JSON 数据包给公众号：
     *
     * {
     *     "ip_list": [
     *         "127.0.0.1",
     *         "127.0.0.2",
     *         "101.226.103.0/25"
     *     ]
     * }
     *
     * 错误时微信会返回错误码等信息，JSON数据包示例如下（该示例为AppID无效错误）:
     *
     * {"errcode":40013,"errmsg":"invalid appid"}
     *
     */
    public static function server($accessToken)
    {
        if (empty($accessToken)) die('Wechat.Common.server: $accessToken is not defined');
        $url = 'https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=' . $accessToken;
        $callback = self::get($url);
        $callback = json_decode($callback);
        if (empty($callback->errcode)) {
            return $callback;
        } else {
            die('Wechat.Common.server: ' . $callback->errcode . ' - ' . $callback->errmsg);
        }
        return false;
    }

    /**
     * 微信卡券
     * 获取 api_ticket
     * api_ticket 是用于调用微信卡券JS API的临时票据，有效期为7200 秒，通过access_token 来获取。
     * 开发者注意事项：
     * 1.此用于卡券接口签名的api_ticket与步骤三中通过config接口注入权限验证配置使用的jsapi_ticket不同。
     * 2.由于获取api_ticket 的api 调用次数非常有限，频繁刷新api_ticket 会导致api调用受限，影响自身业务，开发者需在自己的服务存储与更新api_ticket。
     * @param $accessToken
     * @return bool|mixed|string
     *
     * 数据示例：
     *
     * {
     *     "errcode":0,
     *     "errmsg":"ok",
     *     "ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKdvsdshFKA",
     *     "expires_in":7200
     * }
     *
     */
    public static function ticket($accessToken)
    {
        if (empty($accessToken)) die('Wechat.Common.ticket: $accessToken is not defined');
        $url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=' . $accessToken . '&type=jsapi';
        $result = Common::get($url);
        $result = json_decode($result);
        if ($result->errcode == 0 && !empty($result->ticket)) {
            return $result;
        }
        return false;
    }

    public static function ticketSign($ticket, $url)
    {
        $timestamp = time();
        $nonceStr = self::nonceStr();
        $str = self::arrayToQuery([
            'jsapi_ticket' => $ticket,
            'noncestr' => $nonceStr,
            'timestamp' => $timestamp,
            'url' => $url
        ]);
        $signature = sha1($str);
        return [
            'signature' => $signature,
            'timestamp' => $timestamp,
            'noncestr' => $nonceStr
        ];
    }

    /**
     * 随机字符串
     * @return null|string
     */
    public static function nonceStr()
    {
        $str = '1234567890abcdefghijklmnopqrstuvwxyz';
        $temp = null;
        for ($i = 0; $i < 32; $i++) {
            $num = rand(0, 35);
            $temp .= $str[$num];
        }
        return $temp;
    }

    /**
     * 格式化参数 格式化成 url 参数
     * @param $param
     * @return string
     */
    public static function arrayToQuery($param)
    {
        $buff = '';
        foreach ($param as $k => $v) {
            if ($k != 'sign' && $v != '' && !is_array($v)) {
                $buff .= $k . '=' . $v . '&';
            }
        }
        $buff = trim($buff, '&');
        return $buff;
    }

    /**
     * array 转 xml
     * @param array $arr
     * @return string
     */
    public static function arrayToXml($arr)
    {
        if (!is_array($arr)) die('$arr is not array');
        $xml = '<xml>';
        foreach ($arr as $key => $val) {
            if (is_numeric($val)) {
                $xml .= '<' . $key . '>' . $val . '</' . $key . '>';
            } else {
                $xml .= '<' . $key . '><![CDATA[' . $val . ']]></' . $key . '>';
            }
        }
        $xml .= '</xml>';
        return $xml;
    }

    /**
     * xml 转 array
     * @param $xml
     * @return mixed
     */
    public static function xmlToArray($xml)
    {
        if (!$xml) die('$xml is null');
        # 禁止引用外部 xml 实体
        libxml_disable_entity_loader(true);
        $arr = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $arr;
    }

    public static function paySign($apiKey, $param)
    {
        # 签名步骤一：按字典顺序排序参数
        ksort($param);
        $string = self::arrayToQuery($param);
        # 签名步骤二：在string后加入KEY
        $string = $string . "&key=" . $apiKey;
        # 签名步骤三：MD5加密
        $string = md5($string);
        # 签名步骤四：所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }

    private static function setCurl($url, $setParam)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        $setParam($ch);
        $result = curl_exec($ch);
        if (curl_errno($ch) !== 0) {
            return false;
        }
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($status != 200) {
            return false;
        }
        curl_close($ch);
        return $result;
    }

    /**
     * GET 请求数据
     * @param $url
     * @param int $second
     * @return bool|string
     */
    public static function get($url, $second = 30)
    {
        return self::setCurl($url, function($ch) use($second) {
            curl_setopt($ch, CURLOPT_TIMEOUT, $second);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HEADER, false);
        });
        /*
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        $r = curl_exec($ch);
        if (curl_errno($ch) !== 0) {
            return false;
        }
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($status != 200) {
            return false;
        }
        curl_close($ch);
        return $r;
        */
    }

    /**
     * POST 提交JSON参数 请求数据
     *
     * @param $url
     * @param $param
     * @param int $second
     * @return bool
     */
    public static function postJson($url, $param, $second = 30)
    {
        if (is_array($param)) {
            $param = json_encode($param, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        }
        // if (is_array($param)) $param = http_build_query($param);
        return self::setCurl($url, function($ch) use($url, $second, $param) {
            curl_setopt($ch, CURLOPT_TIMEOUT, $second);
            curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Content-Length: ' . strlen($param)));
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HEADER, false);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
            curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
            if (stripos($url, "https://") !== false) {
                curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
                curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
                curl_setopt($ch, CURLOPT_SSLVERSION, 1);
            }
        });
        /*
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Content-Length: ' . strlen($param)));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
        if (stripos($url, "https://") !== false) {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
            curl_setopt($ch, CURLOPT_SSLVERSION, 1);
        }
        $r = curl_exec($ch);
        if (curl_errno($ch) !== 0) {
            return false;
        }
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($status != 200) {
            return false;
        }
        curl_close($ch);
        return $r;
        */
    }

    /**
     * 以post方式提交xml到对应的接口url
     *
     * @param $url
     * @param $xml # 需要post的xml数据
     * @param bool $cert # 证书文件数组,默认为false
     * @param int $second # url执行超时时间，默认30s
     * @return bool
     */
    public static function postXmlSSL($url, $xml, $cert = false, $second = 30)
    {
        return self::setCurl($url, function($ch) use($xml, $cert, $second) {
            curl_setopt($ch, CURLOPT_TIMEOUT, $second);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HEADER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); # 严格校验
            if ($cert != false) {
                # 设置证书
                # 使用证书  cert 与 key 分别属于两个 pem 文件
                if (isset($cert['cert'])) {
                    curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
                    curl_setopt($ch, CURLOPT_SSLCERT, $cert['cert']);
                }
                if (isset($cert['key'])) {
                    curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
                    curl_setopt($ch, CURLOPT_SSLKEY, $cert['key']);
                }
            }
            # post提交方式
            curl_setopt($ch, CURLOPT_POST, TRUE);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        });
/*
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_TIMEOUT, $second); # 设置超时
        curl_setopt($ch, CURLOPT_HEADER, FALSE); # 设置 header
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); # 要求结果为字符串且输出到屏幕上

        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); # 严格校验

        if ($cert == true) {
            # 设置证书
            # 使用证书  cert 与 key 分别属于两个 pem 文件
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLCERT, app_path('Cert') . DIRECTORY_SEPARATOR . 'apiclient_cert.pem');
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLKEY, app_path('Cert') . DIRECTORY_SEPARATOR . 'apiclient_key.pem');
        }
        # post提交方式
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        # 运行curl
        $data = curl_exec($ch);
        # 返回结果
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            curl_close($ch);
        }
        return false;
        */
    }

    #--- 即将废弃的方法 ---#

    /**
     * 生成签名
     * @param $param
     * @param $key
     * @return mixed 签名
     */
    public static function sign($key, $param)
    {
        # 签名步骤一：按字典顺序排序参数
        ksort($param);
        $string = self::arrayToQuery($param);
        # 签名步骤二：在string后加入KEY
        $string = $string . "&key=" . $key;
        # 签名步骤三：MD5加密
        $string = md5($string);
        # 签名步骤四：所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }

    /**
     * curl 获取数据
     * @param $url
     * @param int $second
     * @return bool|string
     */
    public static function curl($url, $second = 30)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        $r = curl_exec($ch);
        if (curl_errno($ch) !== 0) {
            return false;
        }
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($status != 200) {
            return false;
        }
        curl_close($ch);
        return $r;
    }

    # 以后删除
    public static function curlPost($url, $param, $second = 30)
    {
        if (is_array($param)) {
            $param = http_build_query($param);
        }
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
        if (stripos($url, "https://") !== false) {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
            curl_setopt($ch, CURLOPT_SSLVERSION, 1);
        }
        $r = curl_exec($ch);
        if (curl_errno($ch) !== 0) {
            return false;
        }
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($status != 200) {
            return false;
        }
        curl_close($ch);
        return $r;
    }

    /**
     * 以post方式提交xml到对应的接口url
     *
     * @param string $xml 需要post的xml数据
     * @param string $url url
     * @param bool $cert 是否需要证书，默认不需要
     * @param int $second url执行超时时间，默认30s
     * @return mixed
     * @throws WxPayException
     */
    public static function postXmlCurl($xml, $url, $cert = false, $second = 30)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_TIMEOUT, $second); # 设置超时
        curl_setopt($ch, CURLOPT_HEADER, FALSE); # 设置 header
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); # 要求结果为字符串且输出到屏幕上

        /***
         * //如果有配置代理这里就设置代理
         * if (WxPayConfig::CURL_PROXY_HOST != "0.0.0.0"
         * && WxPayConfig::CURL_PROXY_PORT != 0
         * ) {
         * curl_setopt($ch, CURLOPT_PROXY, WxPayConfig::CURL_PROXY_HOST);
         * curl_setopt($ch, CURLOPT_PROXYPORT, WxPayConfig::CURL_PROXY_PORT);
         * }
         ***/

        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); # 严格校验

        if ($cert == true) {
            # 设置证书
            # 使用证书  cert 与 key 分别属于两个 pem 文件
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLCERT, app_path('Cert') . DIRECTORY_SEPARATOR . 'apiclient_cert.pem');
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLKEY, app_path('Cert') . DIRECTORY_SEPARATOR . 'apiclient_key.pem');
        }
        # post提交方式
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        # 运行curl
        $data = curl_exec($ch);
        # 返回结果
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            curl_close($ch);
            die('curl 出错 错误码:' . $error);
        }
    }
}
?>
